<?php
declare (strict_types = 1);

namespace app\command;

use app\common\JsonException;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;

class ServerAsyncTask extends Command
{
    protected $address;
    protected $server;
    protected $pid_file;
    protected $log_file;

    protected function configure()
    {
        $protocol = empty(config('app.server_asynctask.enable_ssl')) ? 'http' : 'https';
        $host = config('app.server_asynctask.host');
        $port = config('app.server_asynctask.port');
        $this->address = "<{$protocol}://{$host}:{$port}>";

        // 指令配置
        $this->setName('server:asynctask')
            ->addArgument('action', Argument::OPTIONAL, "start|stop|reload|restart|status", 'start')
            ->setDescription('异步任务处理 ' . $this->address);

        $this->log_file = runtime_path() . 'log' . DIRECTORY_SEPARATOR . 'asynctask.log';
        // pid文件不要放到runtime目录，否则容易被清除掉，造成进程无法正常关闭
        $this->pid_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'marks' . DIRECTORY_SEPARATOR . 'asynctask.pid';
    }

    protected function execute(Input $input, Output $output)
    {
        if (!extension_loaded('swoole')) {
            $output->error('Can\'t detect Swoole extension installed.');
            return;
        }
        $action = $input->getArgument('action');
        if (in_array($action, ['start', 'stop', 'reload', 'restart', 'status'])) {
            $this->$action();
        } else {
            $output->error("Invalid argument action:{$action}, Expected start|stop|reload|restart|status.");
            return;
        }
    }

    protected function checkFile($file)
    {
        $dir = dirname($file);
        if (!is_dir($dir)) {
            @mkdir($dir, 0700, true);
        }
        @touch($file);
        if (!is_writable($file)) {
            return false;
        }
        return true;
    }

    protected function start()
    {
        if ($this->isRunning()) {
            $this->output->error('Swoole server is already running.');
            return;
        }
        $this->output->writeln('Starting swoole server...');

        if (!$this->checkFile($this->pid_file)) {
            $this->output->error("Can\'t write file: {$this->pid_file}.");
            return;
        }
        if (!$this->checkFile($this->log_file)) {
            $this->output->error("Can\'t write file: {$this->log_file}.");
            return;
        }

        $sockType = SWOOLE_SOCK_TCP;
        $option = array(
            'worker_num' => 2,
            'task_worker_num' => 4,
            'task_max_request' => 100,
            'pid_file' => $this->pid_file,
            'log_file' => $this->log_file,
            'daemonize' => true,
        );
        if (!empty(config('app.server_asynctask.enable_ssl'))) {
            $sockType = $sockType | SWOOLE_SSL;
            $option['ssl_cert_file'] = config('app.server_asynctask.cert_file');
            $option['ssl_key_file'] = config('app.server_asynctask.key_file');
        }
        $this->server = new \swoole_http_server(config('app.server_asynctask.host'), (int) config('app.server_asynctask.port'), SWOOLE_PROCESS, $sockType);
        $this->server->set($option);

        $this->server->on('Request', function ($request, $response) {
            try {
                $data = json_decode($request->rawContent(), true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    failure(10000);
                }
                if (empty($data)) {
                    failure(10);
                }
                if (!verify_sign($data, config('app.server_asynctask.secret'))) {
                    failure(11);
                }
                if (!verify_timestamp($data, 300)) {
                    failure(12);
                }
                sign_clear($data);
                $this->server->task($data);
                success();
            } catch (JsonException $e) {
                $json = $e->getJson();
                if (is_null($json['msg'])) {
                    $code = $json['code'];
                    $json['msg'] = lang("ERROR_{$code}");
                }
                $response->write(json_encode($json, JSON_UNESCAPED_UNICODE));
            }
        });

        $this->server->on("Task", function ($server, $task_id, $src_worker_id, $data) {
            // $task_id 和 $src_worker_id 组合起来才是全局唯一的，不同的 worker 进程投递的任务 ID 可能会有相同
            var_dump($task_id, $src_worker_id, $data);
        });

        $this->output->writeln("Swoole server started: {$this->address}");

        $this->server->start();
    }

    protected function stop()
    {
        if (!$this->isRunning()) {
            $this->output->error('No swoole server running.');
            return;
        }
        $this->output->writeln('Stopping swoole server...');
        $pid = (int) file_get_contents($this->pid_file);
        \swoole_process::kill($pid, SIGTERM);
        $end = time() + 15;
        while (time() < $end && \swoole_process::kill($pid, 0)) {
            usleep(100000);
        }
        if ($this->isRunning()) {
            $this->output->error('Unable to stop the swoole server.');
            return;
        }
        $this->output->writeln('Stopping success.');
    }

    protected function reload()
    {
        if (!$this->isRunning()) {
            $this->output->error('No swoole server running.');
            return;
        }
        $this->output->writeln('Reloading swoole server...');
        $pid = (int) file_get_contents($this->pid_file);
        if (!\swoole_process::kill($pid, SIGUSR1)) {
            $this->output->error('Reloading failure.');
            return;
        }
        $this->output->writeln('Reloading success.');
    }

    protected function restart()
    {
        if ($this->isRunning()) {
            $this->stop();
        }
        $this->start();
    }

    protected function status()
    {
        $this->output->writeln($this->isRunning() ? 'Server is running.' : 'Server stopped.');
    }

    protected function isRunning()
    {
        if (!is_readable($this->pid_file)) {
            return false;
        }
        $pid = (int) file_get_contents($this->pid_file);
        return $pid > 0 && \swoole_process::kill($pid, 0);
    }
}
